<?php
/*
***************************************************************************
*   Copyright (C) 2007-2008 by Sixdegrees                                 *
*   cesar@sixdegrees.com.br                                               *
*   "Working with freedom"                                                *
*   http://www.sixdegrees.com.br                                          *
*                                                                         *
*   Modified by Ethan Smith (ethan@3thirty.net), April 2008               *
*      - Added support for non-standard port numbers (rewrote cleanURL)   *
*      - getFileLogs will now include an array of files, if multiple      *
*        have been modified files are                                     *
*      - added setRepository method, to fix mis-spelling of old           *
*        setRespository method                                            *
*      - various bugfixes (out by one error on getFileLogs)               *
*                                                                         *
*   Modified by Ethan Smith (ethan@3thirty.net), June 23 2008             *
*      - Removed references to storeFileLogs as a member variable - it's  *
*        now a local variable within getFileLogs() called $fileLogs       * 
*      - getFile() now checks if you are requesting a directory, and      *
*         will return false if you are.                                   *
*      - Added a new parameter to run getDirectoryTree non- recursively   *
*                                                                         *
*   Modified by Per Soderlind (per@soderlind.no), August 13 2008          *
*      - Added support for LP2:BASELINE-RELATIVE-PATH in                  *
*        storeDirectoryFiles()                                            *
*      - In storeDirectoryFiles(), changed if{} elseif {} to switch {}    *
*        since it's faster :)                                             *
*                                                                         *
*   Modified by Dmitrii Shevchenko (dmitrii.shevchenko@gmail.com),        * 
*                                                 August 17 2008          *
*      - minor change to getDirectoryTree() function                      *
*      - added checkOut() function                                        *
*                                                                         *
*   Modified by Rasmus Berg Palm (rasmusbergpalm@gmail.com),              *
*                                                 28 October 2009         *
*       - Fixed 404 error in request() when RequestURI had whitespaces    *  
*                                                                         *
*                                                                         *
*   Permission is hereby granted, free of charge, to any person obtaining *
*   a copy of this software and associated documentation files (the       *
*   "Software"), to deal in the Software without restriction, including   *
*   without limitation the rights to use, copy, modify, merge, publish,   *
*   distribute, sublicense, and/or sell copies of the Software, and to    *
*   permit persons to whom the Software is furnished to do so, subject to *
*   the following conditions:                                             *
*                                                                         *
*   The above copyright notice and this permission notice shall be        *
*   included in all copies or substantial portions of the Software.       *
*                                                                         *
*   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       *
*   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    *
*   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*
*   IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR     *
*   OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, *
*   ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR *
*   OTHER DEALINGS IN THE SOFTWARE.                                       *
***************************************************************************
*/
define( "PHPSVN_DIR", dirname( __FILE__ ) );

require ( PHPSVN_DIR . "/http.php" );
require ( PHPSVN_DIR . "/xml_parser.php" ); // to be dropped?
require ( PHPSVN_DIR . "/definitions.php" );
require ( PHPSVN_DIR . "/xml2Array.php" );

/**
 * PHP SVN CLIENT
 *
 * This class is a SVN client. It can perform read operations
 * to a SVN server (over Web-DAV). 
 * It can get directory files, file contents, logs. All the operaration
 * could be done for a specific version or for the last version.
 *
 * @author Cesar D. Rodas <cesar@sixdegrees.com.br>
 * @license BSD License
 */
class phpsvnclient
{

    /**
     * SVN Repository URL
     *
     * @var string
     * @access private
     */
    private $_url;

    /**
     * Cache, for don't request the same thing in a
     * short period of time.
     *
     * @var string
     * @access private
     */
    private $_cache;

    /**
     * HTTP Client object
     *
     * @var object
     * @access private
     */
    private $_http;

    /**
     * Respository Version.
     *
     * @access private
     * @var interger
     */
    private $_repVersion;

    /**
     * Password
     *
     * @access private
     * @var string
     */
    private $pass;

    /**
     * Password
     *
     * @access private
     * @var string
     */
    private $user;

    /**
     * Last error number
     *
     * Possible values are NOT_ERROR, NOT_FOUND, AUTH_REQUIRED, UNKOWN_ERROR
     *
     * @access public
     * @var integer
     */
    public $errNro;

    private $storeDirectoryFiles = array();

    private $lastDirectoryFiles;

    public function __construct ( $url = 'http://phpsvnclient.googlecode.com/svn/', $user = false, $pass = false )
    {
        $http = & $this->_http;
        $http = new http_class();
        $http->user_agent = "phpsvnclient (http://phpsvnclient.googlecode.com/)";
        
        $this->_url = $url;
        $this->user = $user;
        $this->pass = $pass;
    }

    /**
     * Public Functions
     */
    
    /**
     * checkOut
     */
    public function checkOut ( $folder = '/', $outPath = '.' )
    {
        while ( $outPath[strlen( $outPath ) - 1] == '/' && strlen( $outPath ) > 1 )
            $outPath = substr( $outPath, 0, - 1 );
        $tree = $this->getDirectoryTree( $folder );
        if ( ! file_exists( $outPath ) )
        {
            mkdir( $outPath, 0777, TRUE );
        }
        foreach ( $tree as $file )
        {
            $path = $file['path'];
            $tmp = strstr( trim( $path, '/' ), trim( $folder, '/' ) );
            $createPath = $outPath . '/' . ( $tmp ? substr( $tmp, strlen( trim( $folder, '/' ) ) ) : "" );
            if ( trim( $path, '/' ) == trim( $folder, '/' ) ) continue;
            if ( $file['type'] == 'directory' && ! is_dir( $createPath ) )
            {
                mkdir( $createPath );
            }
            elseif ( $file['type'] == 'file' )
            {
                $contents = $this->getFile( $path );
                $hOut = fopen( $createPath, 'w' );
                fwrite( $hOut, $contents );
                fclose( $hOut );
            }
        }
    }

    /**
     * rawDirectoryDump
     *
     * This method dumps SVN data for $folder
     * in the version $version of the repository.
     *
     * @param string  $folder Folder to get data
     * @param integer $version Repository version, -1 means actual
     * @return array SVN data dump.
     */
    public function rawDirectoryDump ( $folder = '/', $version = -1 )
    {
        $actVersion = $this->getVersion();
        if ( $version == - 1 || $version > $actVersion )
        {
            $version = $actVersion;
        }
        $url = $this->cleanURL( $this->_url . "/!svn/bc/" . $version . "/" . $folder . "/" );
        $this->initQuery( $args, "PROPFIND", $url );
        $args['Body'] = PHPSVN_NORMAL_REQUEST;
        $args['Headers']['Content-Length'] = strlen( PHPSVN_NORMAL_REQUEST );
        
        if ( ! $this->Request( $args, $headers, $body ) )
        {
            return false;
        }
        $xml2Array = new xml2Array();
        return $xml2Array->xmlParse( $body );
    }

    /**
     * getDirectoryFiles
     *
     * This method returns all the files in $folder
     * in the version $version of the repository.
     *
     * @param string  $folder Folder to get files
     * @param integer $version Repository version, -1 means actual
     * @return array List of files.	 */
    public function getDirectoryFiles ( $folder = '/', $version = -1 )
    {
        if ( $arrOutput = $this->rawDirectoryDump( $folder, $version ) )
        {
            /*echo '<pre>';
			print_r($arrOutput);
			echo '</pre>';
			*/
            $files = array();
            foreach ( $arrOutput['children'] as $key => $value )
            {
                //echo $key . ' => ' . $value . '</br>';
                array_walk_recursive( $value, array( 
                    $this, 'storeDirectoryFiles' 
                ) );
                array_push( $files, $this->storeDirectoryFiles );
                unset( $this->storeDirectoryFiles );
            }
            return $files;
        }
        return false;
    }

    /**
     * getDirectoryTree
     *
     * This method returns the complete tree of files and directories
     * in $folder from the version $version of the repository. Can also be used
     * to get the info for a single file or directory
     *
     * @param string  $folder Folder to get tree
     * @param integer $version Repository version, -1 means actual
     * @param boolean $recursive Whether to get the tree recursively, or just
     * the specified directory/file.
     *
     * @return array List of files and directories.
     */
    public function getDirectoryTree ( $folder = '/', $version = -1, $recursive = true )
    {
        $directoryTree = array();
        
        if ( ! ( $arrOutput = $this->getDirectoryFiles( $folder, $version ) ) ) return false;
        
        if ( ! $recursive ) return $arrOutput[0];
        
        while ( count( $arrOutput ) && is_array( $arrOutput ) )
        {
            $array = array_shift( $arrOutput );
            
            array_push( $directoryTree, $array );
            
            if ( trim( $array['path'], '/' ) == trim( $folder, '/' ) ) continue;
            
            if ( $array['type'] == 'directory' )
            {
                $walk = $this->getDirectoryFiles( $array['path'], $version );
                array_shift( $walk );
                //$walk = array_reverse($walk);
                

                foreach ( $walk as $step )
                {
                    array_unshift( $arrOutput, $step );
                }
            }
        }
        return $directoryTree;
    }

    /**
     * Returns file contents
     *
     * @param	string 	$file File pathname
     * @param	integer	$version File Version
     * @return	string	File content and information, false on error, or if a
     * directory is requested
     */
    public function getFile ( $file, $version = -1 )
    {
        $actVersion = $this->getVersion();
        if ( $version == - 1 || $version > $actVersion )
        {
            $version = $actVersion;
        }
        
        // check if this is a directory... if so, return false, otherwise we
        // get the HTML output of the directory listing from the SVN server. 
        // This is maybe a bit heavy since it makes another connection to the
        // SVN server. Maybe add this as an option/parameter? ES 23/06/08
        $fileInfo = $this->getDirectoryTree( $file, $version, false );
        if ( $fileInfo["type"] == "directory" ) return false;
        
        $url = $this->cleanURL( $this->_url . "/!svn/bc/" . $version . "/" . $file . "/" );
        $this->initQuery( $args, "GET", $url );
        if ( ! $this->Request( $args, $headers, $body ) ) return false;
        
        return $body;
    }

    /**
     * Get changes logs of a file.
     *
     * Get repository change logs between version
     * $vini and $vend.
     *
     * @param integer $vini Initial Version
     * @param integer $vend End Version
     * @return Array Respository Logs
     */
    public function getRepositoryLogs ( $vini = 0, $vend = -1 )
    {
        return $this->getFileLogs( "/", $vini, $vend );
    }

    /**
     * Get changes logs of a file.
     *
     * Get repository change of a file between version
     * $vini and $vend.
     *
     * @param
     * @param integer $vini Initial Version
     * @param integer $vend End Version
     * @return Array Respository Logs
     */
    public function getFileLogs ( $file, $vini = 0, $vend = -1 )
    {
        $fileLogs = array();
        
        $actVersion = $this->getVersion();
        if ( $vend == - 1 || $vend > $actVersion ) $vend = $actVersion;
        
        if ( $vini < 0 ) $vini = 0;
        if ( $vini > $vend ) $vini = $vend;
        
        $url = $this->cleanURL( $this->_url . "/!svn/bc/" . $actVersion . "/" . $file . "/" );
        $this->initQuery( $args, "REPORT", $url );
        $args['Body'] = sprintf( PHPSVN_LOGS_REQUEST, $vini, $vend );
        $args['Headers']['Content-Length'] = strlen( $args['Body'] );
        $args['Headers']['Depth'] = 1;
        
        if ( ! $this->Request( $args, $headers, $body ) ) return false;
        
        $xml2Array = new xml2Array();
        $arrOutput = $xml2Array->xmlParse( $body );
        array_shift( $arrOutput['children'] );
        
        foreach ( $arrOutput['children'] as $value )
        {
            $array = array();
            foreach ( $value['children'] as $entry )
            {
                $entry['tagData'] = isset( $entry['tagData'] ) ? $entry['tagData'] : '';
                if ( $entry['name'] == 'D:VERSION-NAME' ) $array['version'] = $entry['tagData'];
                if ( $entry['name'] == 'D:CREATOR-DISPLAYNAME' ) $array['author'] = $entry['tagData'];
                if ( $entry['name'] == 'S:DATE' ) $array['date'] = $entry['tagData'];
                if ( $entry['name'] == 'D:COMMENT' ) $array['comment'] = $entry['tagData'];
                if ( ( $entry['name'] == 'S:ADDED-PATH' ) || ( $entry['name'] == 'S:MODIFIED-PATH' ) || ( $entry['name'] == 'S:DELETED-PATH' ) || ( $entry['name'] == 'S:REPLACED-PATH' ))
                {
                    // For backward compatability
                    $array['files'][] = $entry['tagData'];
                    
                    if ( $entry['name'] == 'S:ADDED-PATH' ) $array['add_files'][] = $entry['tagData'];
                    if ( $entry['name'] == 'S:MODIFIED-PATH' || $entry['name'] == 'S:REPLACED-PATH' ) $array['mod_files'][] = $entry['tagData'];
                    if ( $entry['name'] == 'S:DELETED-PATH' ) $array['del_files'][] = $entry['tagData'];
                }                
            }
            array_push( $fileLogs, $array );
        }
        
        return $fileLogs;
    }

    /**
     * Get the repository version
     *
     * @return integer Repository version
     * @access public
     */
    public function getVersion ( )
    {
        if ( $this->_repVersion > 0 ) return $this->_repVersion;
        
        $this->_repVersion = - 1;
        $this->initQuery( $args, "PROPFIND", $this->cleanURL( $this->_url . "/!svn/vcc/default" ) );
        $args['Body'] = PHPSVN_VERSION_REQUEST;
        $args['Headers']['Content-Length'] = strlen( PHPSVN_NORMAL_REQUEST );
        $args['Headers']['Depth'] = 0;
        
        if ( ! $this->Request( $args, $tmp, $body ) )
        {
            return $this->_repVersion;
        }
        
        $parser = new xml_parser_class();
        $parser->Parse( $body, true );
        $enable = false;
        foreach ( $parser->structure as $value )
        {
            if ( $enable )
            {
                $t = explode( "/", $value );
                
                // start from the end and move backwards until we find a non-blank entry
                $index = count( $t ) - 1;
                while ( $t[$index] == "" )
                {
                    $index --;
                }
                
                // check the last non-empty element to see if it's numeric. If so, it's the revision number
                if ( is_numeric( $t[$index] ) )
                {
                    $this->_repVersion = $t[$index];
                    break;
                }
                else
                {
                    $enable = false;
                    continue;
                }
            }
            if ( is_array( $value ) && $value['Tag'] == 'D:href' ) $enable = true;
        }
        return $this->_repVersion;
    }

    /**
     * Deprecated functions for backward comatability
     */
    
    /**
     * Set URL
     *
     * Set the project repository URL.
     *
     * @param string $url URL of the project.
     * @access public
     */
    public function setRepository ( $url )
    {
        $this->_url = $url;
    }

    /**
     * Old method; there's a typo in the name. This is now a wrapper for setRepository
     */
    public function setRespository ( $url )
    {
        return $this->setRepository( $url );
    }

    /**
     * Add Authentication  settings
     *
     * @param string $user Username
     * @param string $pass Password
     */
    public function setAuth ( $user, $pass )
    {
        $this->user = $user;
        $this->pass = $pass;
    }

    /**
     * Private Functions
     */
    /**
     * Callback for array_walk_recursive in public function getDirectoryFiles
     *
     * @access private
     */
    private function storeDirectoryFiles ( $item, $key )
    {
        if ( $key == 'name' )
        {
            if ( ( $item == 'D:HREF' ) || ( $item == 'LP1:GETLASTMODIFIED' ) || ( $item == 'LP2:BASELINE-RELATIVE-PATH' ) || ( $item == 'LP3:BASELINE-RELATIVE-PATH' ) || ( $item == 'D:STATUS' ) )
            {
                $this->lastDirectoryFiles = $item;
            }
        }
        elseif ( ( $key == 'tagData' ) && ( $this->lastDirectoryFiles != '' ) )
        {
            
            // Unsure if the 1st of two D:HREF's always returns the result we want, but for now...
            if ( ( $this->lastDirectoryFiles == 'D:HREF' ) && ( isset( $this->storeDirectoryFiles['type'] ) ) ) return;
            
            // Dump into the array 
            switch ( $this->lastDirectoryFiles )
            {
                case 'D:HREF':
                    $var = 'type';
                    break;
                case 'LP1:GETLASTMODIFIED':
                    $var = 'last-mod';
                    break;
                case 'LP2:BASELINE-RELATIVE-PATH':
                case 'LP3:BASELINE-RELATIVE-PATH':
                    $var = 'path';
                    break;
                case 'D:STATUS':
                    $var = 'status';
                    break;
            }
            $this->storeDirectoryFiles[$var] = $item;
            $this->lastDirectoryFiles = '';
            
            // Detect 'type' as either a 'directory' or 'file'
            if ( ( isset( $this->storeDirectoryFiles['type'] ) ) && ( isset( $this->storeDirectoryFiles['last-mod'] ) ) && ( isset( $this->storeDirectoryFiles['path'] ) ) && ( isset( $this->storeDirectoryFiles['status'] ) ) )
            {
                $this->storeDirectoryFiles['path'] = str_replace( ' ', '%20', $this->storeDirectoryFiles['path'] ); //Hack to make filenames with spaces work.
                $len = strlen( $this->storeDirectoryFiles['path'] );
                if ( substr( $this->storeDirectoryFiles['type'], strlen( $this->storeDirectoryFiles['type'] ) - $len ) == $this->storeDirectoryFiles['path'] )
                {
                    $this->storeDirectoryFiles['type'] = 'file';
                }
                else
                {
                    $this->storeDirectoryFiles['type'] = 'directory';
                }
            }
        
        }
        else
        {
            $this->lastDirectoryFiles = '';
        }
    }

    /**
     * Prepare HTTP CLIENT object
     *
     * @param array &$arguments Byreferences variable.
     * @param string $method Method for the request (GET,POST,PROPFIND, REPORT,ETC).
     * @param string $url URL for the action.
     * @access private
     */
    private function initQuery ( &$arguments, $method, $url )
    {
        $http = & $this->_http;
        $http->GetRequestArguments( $url, $arguments );
        if ( isset( $this->user ) && isset( $this->pass ) )
        {
            $arguments["Headers"]["Authorization"] = " Basic " . base64_encode( $this->user . ":" . $this->pass );
        }
        $arguments["RequestMethod"] = $method;
        $arguments["Headers"]["Content-Type"] = "text/xml";
        $arguments["Headers"]["Depth"] = 1;
    }

    /**
     * Open a connection, send request, read header
     * and body.
     *
     * @param Array $args Connetion's argument
     * @param Array &$headers Array with the header response.
     * @param string &$body Body response.
     * @return boolean True is query success
     * @access private
     */
    private function Request ( $args, &$headers, &$body )
    {
        $args['RequestURI'] = str_replace( ' ', '%20', $args['RequestURI'] ); //Hack to make filenames with spaces work.
        $http = & $this->_http;
        $http->Open( $args );
        $http->SendRequest( $args );
        $http->ReadReplyHeaders( $headers );
        if ( $http->response_status[0] != 2 )
        {
            switch ( $http->response_status )
            {
                case 404:
                    $this->errNro = NOT_FOUND;
                    break;
                case 401:
                    $this->errNro = AUTH_REQUIRED;
                    break;
                default:
                    $this->errNro = UNKNOWN_ERROR;
            }
            $http->close();
            return false;
        }
        $this->errNro = NO_ERROR;
        $body = '';
        $tbody = '';
        for (;;)
        {
            $error = $http->ReadReplyBody( $tbody, 1000 );
            if ( $error != "" || strlen( $tbody ) == 0 ) break;
            $body .= ( $tbody );
        }
        $http->close();
        return true;
    }

    /**
     * Clean URL
     *
     * Delete "//" on URL requests.
     *
     * @param string $url URL
     * @return string New cleaned URL.
     * @access private
     */
    private function cleanURL ( $url )
    {
        return preg_replace( "/((^:)\/\/)/", "//", $url );
    }
}
?>
